package cn.net.xyan.secrity.acme;

import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import java.io.*;
import java.net.URI;
import java.security.KeyPair;


public class CertBot {

    private static final Logger LOG = LoggerFactory.getLogger(CertBot.class);

//    /**
//     * Loads a user key pair from {@value #USER_KEY_FILE}. If the file does not exist,
//     * a new key pair is generated and saved.
//     * <p>
//     * Keep this key pair in a safe place! In a production environment, you will not be
//     * able to access your account again if you should lose the key pair.
//     *
//     * @return User's {@link KeyPair}.
//     */
//    private KeyPair loadOrCreateUserKeyPair() throws IOException {
//        if (USER_KEY_FILE.exists()) {
//            // If there is a key file, read it
//            try (FileReader fr = new FileReader(USER_KEY_FILE)) {
//                return KeyPairUtils.readKeyPair(fr);
//            }
//
//        } else {
//            // If there is none, create a new key pair and save it
//            KeyPair userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
//            try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
//                KeyPairUtils.writeKeyPair(userKeyPair, fw);
//            }
//            return userKeyPair;
//        }
//    }

//    /**
//     * Loads a domain key pair from {@value #DOMAIN_KEY_FILE}. If the file does not exist,
//     * a new key pair is generated and saved.
//     *
//     * @return Domain {@link KeyPair}.
//     */
//    private KeyPair loadOrCreateDomainKeyPair() throws IOException {
//        if (DOMAIN_KEY_FILE.exists()) {
//            try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {
//                return KeyPairUtils.readKeyPair(fr);
//            }
//        } else {
//            KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
//            try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
//                KeyPairUtils.writeKeyPair(domainKeyPair, fw);
//            }
//            return domainKeyPair;
//        }
//    }

    /**
     * Finds your {@link Account} at the ACME server. It will be found by your user's
     * public key. If your key is not known to the server yet, a new account will be
     * created.
     * <p>
     * This is a simple way of finding your {@link Account}. A better way is to get the
     * URL and KeyIdentifier of your new account with {@link Account#getLocation()}
     * {@link Session#getKeyIdentifier()} and store it somewhere. If you need to get
     * access to your account later, reconnect to it via
     * {@link Account#bind(Session, URI)} by using the stored location.
     *
     * @param session
     *            {@link Session} to bind with
     * @return {@link Login} that is connected to your account
     */
    private Account findOrRegisterAccount(Session session, AcmeCertReq req) throws AcmeException {
        // Ask the user to accept the TOS, if server provides us with a link.
        URI tos = session.getMetadata().getTermsOfService();
        if (tos != null) {
            //acceptAgreement(tos);
        }

        Account account = new AccountBuilder()
                .agreeToTermsOfService()
                .useKeyPair(req.getUserKey())
                .create(session);

        LOG.info("Registered a new user, URL: " + account.getLocation());

        return account;
    }

    /**
     * Authorize a domain. It will be associated with your account, so you will be able to
     * retrieve a signed certificate for the domain later.
     *
     * @param auth
     *            {@link Authorization} to perform
     */
    private void authorize(Authorization auth,AcmeCertReq req) throws AcmeException {
        LOG.info("Authorization for domain " + auth.getDomain());

        // The authorization is already valid. No need to process a challenge.
        if (auth.getStatus() == Status.VALID) {
            return;
        }

        // Find the desired challenge and prepare it.
        if (req.handler == null) throw new NullPointerException("Challenge Handler can't be null!");

        Challenge challenge = req.handler.handlerChallenge(auth,this);

        if (challenge == null) {
            throw new AcmeException("No challenge found");
        }

        // If the challenge is already verified, there's no need to execute it again.
        if (challenge.getStatus() == Status.VALID) {
            return;
        }

        // Now trigger the challenge.
        challenge.trigger();

        // Poll for the challenge to complete.
        try {
            int attempts = 10;
            while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
                // Did the authorization fail?
                if (challenge.getStatus() == Status.INVALID) {
                    throw new AcmeException("Challenge failed... Giving up.");
                }

                // Wait for a few seconds
                Thread.sleep(3000L);

                // Then update the status
                challenge.update();
            }
        } catch (InterruptedException ex) {
            LOG.error("interrupted", ex);
            Thread.currentThread().interrupt();
        }

        // All reattempts are used up and there is still no valid authorization?
        if (challenge.getStatus() != Status.VALID) {
            throw new AcmeException("Failed to pass the challenge for domain "
                    + auth.getDomain() + ", ... Giving up.");
        }
    }

    /**
     * Presents the instructions for preparing the challenge validation, and waits for
     * dismissal. If the user cancelled the dialog, an exception is thrown.
     *
     * @param message
     *            Instructions to be shown in the dialog
     */
    public void acceptChallenge(String message) throws AcmeException {
        int option = JOptionPane.showConfirmDialog(null,
                message,
                "Prepare Challenge",
                JOptionPane.OK_CANCEL_OPTION);
        if (option == JOptionPane.CANCEL_OPTION) {
            throw new AcmeException("User cancelled the challenge");
        }
    }




    public String fetchCertificate(AcmeCertReq req) throws IOException, AcmeException {

        // Create a session for Let's Encrypt.
        // Use "acme://letsencrypt.org" for production server
        Session session = new Session(req.getAcmeURL());

        // Get the Account.
        // If there is no account yet, create a new one.
        Account acct = findOrRegisterAccount(session, req);

        // Load or create a key pair for the domains. This should not be the userKeyPair!
        KeyPair domainKeyPair = req.getDomainKey();

        // Order the certificate
        Order order = acct.newOrder().domains(req.getDomains()).create();

        // Perform all required authorizations
        for (Authorization auth : order.getAuthorizations()) {
            authorize(auth,req);
        }

        // Generate a CSR for all of the domains, and sign it with the domain key pair.
        CSRBuilder csrb = new CSRBuilder();
        csrb.addDomains(req.getDomains());
        csrb.sign(domainKeyPair);

        // Write the CSR to a file, for later use.
        if (req.getCsrWriter() != null) {
            csrb.write(req.getCsrWriter());
        }

        // Order the certificate
        order.execute(csrb.getEncoded());

        // Wait for the order to complete
        try {
            int attempts = 10;
            while (order.getStatus() != Status.VALID && attempts-- > 0) {
                // Did the order fail?
                if (order.getStatus() == Status.INVALID) {
                    throw new AcmeException("Order failed... Giving up.");
                }

                // Wait for a few seconds
                Thread.sleep(3000L);

                // Then update the status
                order.update();
            }
        } catch (InterruptedException ex) {
            LOG.error("interrupted", ex);
            Thread.currentThread().interrupt();
        }

        // Get the certificate
        Certificate certificate = order.getCertificate();

        LOG.info("Success! The certificate for domains " + req.getDomains() + " has been generated!");
        LOG.info("Certificate URL: " + certificate.getLocation());

        try(StringWriter sw = new StringWriter()){
            certificate.writeCertificate(sw);
            sw.flush();
            String body = sw.toString();

            if (req.getOutputWriter()!=null)
                req.getOutputWriter().write(body);

            return body;
        }
        // Write a combined file containing the certificate and chain.
       // try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {

       // }

        // That's all! Configure your web server to use the DOMAIN_KEY_FILE and
        // DOMAIN_CHAIN_FILE for the requested domans.
    }

}
